Definiciones y Tipos Básicos
Un programa en PF consiste esencialmente de una lista de definiciones (de funciones) que se expresan en una notación semejante a la usada en matemática.
Tipado fuerte: toda expresión debe tener un tipo. Todas las constantes, variables, operadores y funciones tienen un tipo asociado.
Chequeo estático de tipos: los tipos se chequean en tiempo de compilación.
Type safety: al ejecutar, los programas bien tipados nunca se trancan por errores de tipo
El tipo de los booleanos es Bool y sus constantes son denotadas True y False.
Las operaciones primitivas son las habituales: not, && y ||.
Los operadores sobre enteros son los habituales: +, −, ∗,ˆ, div, mod, abs, negate.
La conversión entre los dos tipos de enteros se debe hacer en forma explícita a través de las funciones:
fromInteger :: Integer -> Int
toInteger :: Int -> Integer
Los caracteres son codificados por la tabla ASCII.
Se escriben entre comillas simples: ’a’, ’H’, ’8’.
La conversión entre un carácter y su codificación se hace a través de las siguientes funciones:
fromEnum :: Char -> Int
toEnum :: Int -> Char
El tipo String se define como una lista de caracteres
Constantes de tipo String se escriben entre comillas dobles: "Funcional"
, "\tHaskell\n"
.
Las funciones show y read permiten convertir entre valores de otros tipos y String.
Existen 2 tipos que representan los reales en punto flotante: Float y Double.
Las operaciones sobre estos tipos son las estándar (aritméticas, trigonométricas, etc).
Operador de división real: /
Hay funciones de conversión hacia y desde los enteros:
ceiling :: Float -> Integer
floor :: Float -> Integer
round :: Float -> Integer
fromInteger :: Integer -> Float
fromIntegral :: Int -> Float
Además de los tipos básicos, podemos definir tipos estructurados compuestos por un conjunto de valores.
Una tupla combina en un mismo objeto a un número fijo de valores, cada uno con un tipo determinado (posiblemente distintos).
Similar a Registros o Estructuras en otros lenguajes.
Coordenadas cartesianas:
(8, 23) :: (Int, Int)
Empleados (nombre, edad, sueldo):
("Juan", 23, 60000) :: (String, Int, Int)
En general, el tipo (t1, ...,tn)
consiste en tuplas de valores: (v1, ..., vn)
donde v1 :: t1
, ..., vn :: tn
.
Se suele usar pattern matching:
nombre :: (String, Int, Int) -> String
nombre (n, e, s) = n
Se pueden ignorar componentes.
edad (_, e, _) = e
sueldo (_, _, s) = s
Se pueden forzar componentes usando literales.
esPedro ("Pedro", _, _) = True
esPedro (_ , _, _) = False
Una lista combina en un mismo objeto a un número arbitrario de valores, todos de un mismo tipo.
En general, el tipo [t]
consiste en listas de valores: [v1, ..., vn ]
donde v1 :: t, ..., vn :: t
Definimos inductivamente a las listas de tipo [t]
como:
[ ] - lista vacía
v : vs - lista con al menos un elemento, donde v :: t y vs :: [t]
Ejemplos: (++)
, (!!)
, length
, replicate
, take
, drop
, reverse
, etc.
Nuevas funciones se definen combinando las anteriores o usando pattern matching.
dupList xs = xs ++ xs
null [] = True
null (_:_) = False
head (x:_) = x
tail (_:xs) = xs
Notación que permite construir una lista a partir de una descripción de la misma en términos de otra.
[x | x ← [1, 2, 3, 4]] evalua a [1, 2, 3, 4]
[x | x ← [1, 2, 3, 4], isEven x ] evalua a [2, 4]
[isEven x | x ← [1, 2, 3, 4]] evalua a [False,True, False,True ]
[(x + y) | (x, y) ← [(1, 2),(2, 3),(3, 4)]] evalua a [3, 5, 7]
[(x, y) | x ← [1, 2], y ← [3, 4]] evalua a [(1, 3),(1, 4),(2, 3),(2, 4)]
En Haskell podemos dar nombres a los tipos.
Al usar un sinónimo se tiene el mismo efecto que al usar el tipo que representa.
type Coord = (Int, Int)
type Empleado = (String, Int, Int)
Mediante la definición de tipos de datos algebraicos podemos introducir nuevos tipos.
Nos permiten definir:
data PuntoCardinal = Norte | Sur | Este | Oeste
deriving (Show, Eq)
data Empleado = Empleado String Int Int
deriving (Show, Eq)
data Forma = Circulo Float
| Rectangulo Float Float
deriving (Show, Eq)
En general:
data T = C1 t11 ... t1k1
...
| Cn tn1 ... tnkn
Donde T es el nuevo tipo a introducir y cada Ci es un constructor de dicho tipo, que toma ki parámetros.
Función de ejemplo:
area :: Forma → Float
area (Circulo r) = 3.14 ∗ r ∗ r
area (Rectangulo b a) = b ∗ a
cuadrado x = Rectangulo x x
El polimorfismo paramétrico es un mecanismo de tipado que permite definir funciones y tipos de datos de forma paramétrica de forma tal que puedan manipular valores de distintos tipos de forma totalmente uniforme.
head (x : xs) = x
head [1, 2, 3, 4] ----> 1
head [False,True, False ] ----> False
head [’a’, ’b’, ’c’, ’d’] ----> ’a’
El tipo de head es entonces: head :: [a] -> a
, donde a es una variable de tipo.
head :: [a] -> a
es el tipo más general de head.
Al reemplazar las variables de tipo por tipos particulares se obtienen instancias del tipo:
head :: [Int] -> Int
head :: [Bool] -> Bool
head :: [Char] -> Char
No son instancias:
head :: [Int] -> Char
head :: Bool -> Bool
head :: [a] -> Int
Haskell es capaz de inferir el tipo más general de una función.
Las siguientes son algunas funciones polimórficas sobre listas contenidas en el Prelude:
(:) :: a -> [a] -> [a]
(++) :: [a] -> [a] -> [a]
(!!) :: [a] -> Int -> a
last :: [a] -> a
concat :: [[a]] -> [a]
take :: Int -> [a] -> [a]
splitAt :: Int -> [a] -> ([a], [a])
reverse :: [a] -> [a]
zip :: [a] -> [b] -> [(a, b)]
unzip :: [(a, b)] -> ([a], [b])
La sobrecarga es otra forma de polimorfismo, dentro del llamado polimorfismo ad-hoc. Una función sobrecargada puede comportarse de forma diferente (tener una definición diferente) para cada tipo.
En Haskell se utiliza el mismo operador (==)
para representar la igualdad en más de un tipo.
(==) :: Int -> Int -> Bool
(==) :: Char -> Char -> Bool
(==) :: Bool -> Bool -> Bool
En Haskell, la definición de funciones sobrecargadas se hace a través del concepto de clase.
El operador (==)
tiene el siguiente tipo:
(==) :: Eq a => a -> a -> Bool
Una clase especifica una colección de tipos a los que se les asocia un conjunto de operaciones (comunmente llamados métodos).
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x /= y = not (x == y)
x == y = not (x /= y)
Para declarar que un tipo es miembro de una clase hay que definir una instancia.
data T = A | B
instance Eq T where
A == A = True
B == B = True
_ == _ = False
Para un número restricto de clases (como Eq, Ord, Show, etc), se puede pedir que Haskell derive la instancia de una clase en forma automática.
data T = A | B
deriving (Eq, Show)
Una función de alto orden es una función que toma una función como parámetro o retorna una función como resultado.
En PF las funciones son ciudadanas de primera clase.
Ejemplos de funciones de alto orden:
map :: (a -> b) -> [a] -> [b]
filter :: (a -> Bool) -> [a] -> [a]
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
Pero también take :: Int -> [a] -> [a]
es de alto orden debido a que su tipo equivale a:
take :: Int -> ([a] -> [a])
dado que → asocia a la derecha.
Las funciones en Haskell se representan en forma currificada:
las funciones tienen sólo un parámetro.
take :: Int → ([a] → [a])
(take 3) :: [a] → [a]
(take 3) [’a’, ’b’, ’c’, ’d’, ’e’]
-- Los paréntesis no son necesarios, porque la aplicación asocia a la izquierda
take 3 [’a’, ’b’, ’c’, ’d’, ’e’]
-- No currificadas
add :: Num a => (a, a) -> a
add (x, y) = x + y
mult :: Num a => (a, a, a) -> a
mult (x, y, z) = x ∗ y ∗ z
-- Currificadas
addc :: Num a => a -> a -> a
addc x y = x + y
multc :: Num a => a -> a -> a -> a
multc x y z = x ∗ y ∗ z
Se puede pasar de representación currificada a no currificada, y viceversa, usando las siguientes funciones:
curry :: ((a, b) -> c) -> (a -> b -> c)
curry f x y = f (x, y)
uncurry :: (a -> b -> c) -> ((a, b) -> c)
uncurry f (x, y) = f x y
addc = curry add
add = uncurry addc